#!/usr/bin/python3
# -*- coding: utf-8 -*-

import json
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from pathlib import Path
from sys import exit, stderr

import tomli as tomllib

def main():
    args = parse_args()
    problem = False
    if not args.tree.is_dir():
        return f"Not a directory: {args.tree}"
    for pjpath in args.tree.glob("**/package.json"):
        name, version, license = parse(pjpath)
        identity = f"{name} {version}"
        if version in args.exceptions.get(name, ()):
            continue  # Do not even check the license
        elif license is None:
            problem = True
            print(
                f"Missing license in package.json for {identity}", file=stderr
            )
        elif isinstance(license, dict):
            if isinstance(license.get("type"), str):
                continue
            print(
                (
                    "Missing type for (deprecated) license object in "
                    f"package.json for {identity}: {license}"
                ),
                file=stderr,
            )
        elif isinstance(license, list):
            if license and all(
                isinstance(entry, dict) and isinstance(entry.get("type"), str)
                for entry in license
            ):
                continue
            print(
                (
                    "Defective (deprecated) licenses array-of objects in "
                    f"package.json for {identity}: {license}"
                ),
                file=stderr,
            )
        elif isinstance(license, str):
            continue
        else:
            print(
                (
                    "Weird type for license in "
                    f"package.json for {identity}: {license}"
                ),
                file=stderr,
            )
        problem = True
    if problem:
        return "At least one missing license was found."


def check_exception(exceptions, name, version):
    x = args.exceptions


def parse(package_json_path):
    with package_json_path.open("rb") as pjfile:
        pj = json.load(pjfile)
    try:
        license = pj["license"]
    except KeyError:
        license = pj.get("licenses")
    try:
        name = pj["name"]
    except KeyError:
        name = package_json_path.parent.name
    version = pj.get("version", "<unknown version>")

    return name, version, license


def parse_args():
    parser = ArgumentParser(
        formatter_class=RawDescriptionHelpFormatter,
        description=(
            "Search for bundled dependencies without declared licenses"
        ),
        epilog="""

The exceptions file must be a TOML file with zero or more tables. Each table’s
keys are package names; the corresponding values values are exact version
number strings, or arrays of version number strings, that have been manually
audited to determine their license status and should therefore be ignored.

Exceptions in a table called “any” are always applied. Otherwise, exceptions
are applied only if a corresponding --with TABLENAME argument is given;
multiple such arguments may be given.

For
example:

    [any]
    example-foo = "1.0.0"

    [prod]
    example-bar = [ "2.0.0", "2.0.1",]

    [dev]
    example-bat = [ "3.7.4",]

would always ignore version 1.0.0 of example-foo. It would ignore example-bar
2.0.1 only when called with “--with prod”.

Comments may (and should) be used to describe the manual audits upon which the
exclusions are based.

Otherwise, any package.json with missing or null license field in the tree is
considered an error, and the program returns with nonzero status.
""",
    )
    parser.add_argument(
        "-x",
        "--exceptions",
        type=FileType("rb"),
        help="Manually audited package versions file",
    )
    parser.add_argument(
        "-w",
        "--with",
        action="append",
        default=[],
        help="Enable a table in the exceptions file",
    )
    parser.add_argument(
        "tree",
        metavar="node_modules_dir",
        type=Path,
        help="Path to search recursively",
        default=".",
    )
    args = parser.parse_args()

    if args.exceptions is None:
        args.exceptions = {}
        xname = None
    else:
        with args.exceptions as xfile:
            xname = getattr(xfile, "name", "<exceptions>")
            args.exceptions = tomllib.load(args.exceptions)
        if not isinstance(args.exceptions, dict):
            parser.error(f"Invalid format in {xname}: not an object")
        for tablename, table in args.exceptions.items():
            if not isinstance(table, dict):
                parser.error(
                    f"Non-table entry in {xname}: {tablename} = {table!r}"
                )
            overlay = {}
            for key, value in table.items():
                if isinstance(value, str):
                    overlay[key] = [value]
                elif not isinstance(value, list) or not all(
                    isinstance(entry, str) for entry in value
                ):
                    parser.error(
                        f"Invalid format in {xname} in [{tablename}]: "
                        f"{key!r} = {value!r}"
                    )
            table.update(overlay)

    x = args.exceptions.get("any", {})
    for add in getattr(args, "with"):
        try:
            x.update(args.exceptions[add])
        except KeyError:
            if xname is None:
                parser.error(
                    f"No table {add}, as no exceptions file was given"
                )
            else:
                parser.error(f"No table {add} in {xname}")
    # Store the merged dictionary
    args.exceptions = x

    return args


if __name__ == "__main__":
    exit(main())
